#!/usr/bin/env bash

# rmate
# Copyright (C) 2011-present by Harald Lapp <harald@octris.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

#
# This script can be found at:
# https://github.com/aurora/rmate
#

#
# This script is a pure bash compatible shell script implementing remote
# textmate functionality
#

#
# Thanks very much to all users and contributors! All bug-reports,
# feature-requests, patches, etc. are greatly appreciated! :-)
#

# init
#
version="1.0.2"
version_date="2019-04-08"
version_string="rmate-sh $version ($version_date)"

# determine hostname
function hostname_command(){
    if command -v hostname >/dev/null 2>&1; then
        echo "hostname"
    else {
        HOSTNAME_DESCRIPTOR="/proc/sys/kernel/hostname"
        if test -r "$HOSTNAME_DESCRIPTOR"; then
            echo "cat $HOSTNAME_DESCRIPTOR"
        else
            echo "hostname"
        fi
        }
    fi
}

hostname=$($(hostname_command))

# default configuration
host=localhost
port=52698
eval home=$(builtin printf "~%q" "${SUDO_USER:-$LOGNAME}")

function load_config {
    local rc_file=$1
    local row

    local host_pattern="^host(:[[:space:]]+|=)([^ ]+)"
    local port_pattern="^port(:[[:space:]]+|=)([0-9]+)"

    if [ -f "$rc_file" ]; then
        while read -r row; do
            if [[ "$row" =~ $host_pattern ]]; then
                host=${BASH_REMATCH[2]}
            elif [[ "$row" =~ $port_pattern ]]; then
                port=${BASH_REMATCH[2]}
            fi
        done < "$rc_file"
    fi
}

for i in "/etc/${0##*/}" $home/."${0##*/}/${0##*/}.rc" $home/."${0##*/}.rc"; do
    load_config $i
done

host="${RMATE_HOST:-$host}"
port="${RMATE_PORT:-$port}"

tmp_dir="${TMPDIR:-/tmp}"

# misc initialization
filepaths=()
displaynames=()
selections=()
filetypes=()
verbose=false
nowait=true
force=false
success=false

# process command-line parameters
#
function showusage {
    echo "usage: $(basename $0) [arguments] [--] file-path  edit specified file
   or: $(basename $0) [arguments] -          read text from stdin

-H, --host HOST  Connect to HOST. Use 'auto' to detect the host from
                 SSH. Defaults to $host.
-p, --port PORT  Port number to use for connection. Defaults to $port.
-w, --[no-]wait  Wait for file to be closed by TextMate.
-l, --line LINE  Place caret on line number after loading file.
+N               Alias for --line, if N is a number (eg.: +5).
-m, --name NAME  The display name shown in TextMate.
-t, --type TYPE  Treat file as having specified type.
-n, --new        Open in a new window (Sublime Text).
-f, --force      Open even if file is not writable.
-v, --verbose    Verbose logging messages.
-h, --help       Display this usage information.
    --version    Show version and exit.
"
}

function log {
    if [[ $verbose = true ]]; then
        echo "$@" 1>&2
    fi
}

function dirpath {
    (cd "$(dirname "$1")" >/dev/null 2>/dev/null || { echo "unable to cd to $1 directory" 1>&2; exit; } ; pwd -P)
}

function resolvepath {
    local filepath="$1"
    local directory

    while [ -L "$filepath" ]; do
        directory="$(cd -P "$(dirname "$filepath")" && pwd)"
        filepath="$(readlink "$filepath")"

        if [[ "${filepath:0:1}" != "/" ]]; then
            filepath="$directory/$filepath"
        fi
    done

    echo "$filepath"
}

function canonicalize {
    local filepath="$1"
    local relativepath
    local result

    if [[ "${filepath:0:1}" = "-" ]]; then
        filepath="./$filepath"
    fi

    local dir=$(dirpath "$filepath")

    if [ -L "$filepath" ]; then
        relativepath=$(cd "$dir" || { echo "unable to cd to $dir" 1>&2; exit; } ; resolvepath "$(basename "$filepath")")
        result=$(dirpath "$relativepath")/$(basename "$relativepath")
    else
        result=$(basename "$filepath")
        if [ "$dir" = '/' ]; then
            result="$dir$result"
        else
            result="$dir/$result"
        fi
    fi

    echo "$result"
}

while [[ "$1" != "" ]]; do
    case $1 in
        -)
            filepaths+=("-")
            ;;
        --)
            shift
            break
            ;;
        -H|--host)
            host=$2
            shift
            ;;
        -p|--port)
            port=$2
            shift
            ;;
        -w|--wait)
            nowait=false
            ;;
        --no-wait)
            nowait=true
            ;;
        -l|--line)
            selections+=($2)
            shift
            ;;
        -m|--name)
            displaynames+=($2)
            shift
            ;;
        -t|--type)
            filetypes+=($2)
            shift
            ;;
        -n|--new)
            new=true
            ;;
        -f|--force)
            force=true
            ;;
        -v|--verbose)
            verbose=true
            ;;
        --version)
            echo "$version_string"
            exit 0
            ;;
        -h|-\?|--help)
            showusage
            exit 0
            ;;
        +[0-9][0-9]*)
            selections+=(${1:1})
            ;;
        *)
            if [[ "${1:0:1}" = "-" ]]; then
                showusage
                exit 1
            else
                filepaths+=("$1")
            fi
            ;;
    esac

    shift
done

if [[ "$host" = "auto" && "$SSH_CONNECTION" != "" ]]; then
    host=${SSH_CONNECTION%% *}
fi

filepaths=("${filepaths[@]}" "$@")

if [ "${filepaths[*]}" = "" ]; then
    if [[ $nowait = false ]]; then
        filepaths=('-')
    else
        case "$-" in
            *i*)
                showusage
                exit 1
                ;;
            *)
                filepaths=('-')
                ;;
        esac
    fi
fi


#------------------------------------------------------------
# main
#------------------------------------------------------------

function open_file {
    local index="$1"
    local filepath="${filepaths[$index]}"
    local selection="${selections[$index]}"
    local filetype="${filetypes[$index]}"
    local displayname="${displaynames[$index]}"
    local realpath
    local data

    if [ "$filepath" != "-" ]; then
        realpath=$(canonicalize "$filepath")
        log "$realpath"

        if [ -d "$filepath" ]; then
            echo "$filepath is a directory and rmate is unable to handle directories."
            return 1
        fi

        if [ -f "$realpath" ] && [ ! -w "$realpath" ]; then
            if [[ $force = false ]]; then
                echo "File $filepath is not writable! Use -f to open anyway."
                return 1
            elif [[ $verbose = true ]]; then
                log "File $filepath is not writable! Opening anyway."
            fi
        fi

        if [ "$displayname" = "" ]; then
            displayname="$hostname:$filepath"
        fi
    else
        displayname="$hostname:untitled"
    fi

    echo "open" 1>&3
    echo "display-name: $displayname" 1>&3
    echo "real-path: $realpath" 1>&3
    echo "data-on-save: yes" 1>&3
    echo "re-activate: yes" 1>&3
    echo "token: $filepath" 1>&3

    if [[ $new = true ]]; then
        echo "new: yes"  1>&3
    fi

    if [ "$selection" != "" ]; then
        echo "selection: $selection" 1>&3
    fi

    if [ "$filetype" != "" ]; then
        echo "file-type: $filetype" 1>&3
    fi

    if [ "$filepath" != "-" ] && [ -f "$filepath" ]; then
        filesize=$(($(wc -c <"$realpath")))
        echo "data: $filesize" 1>&3
        cat "$realpath" 1>&3
    elif [ "$filepath" = "-" ]; then
        if [ -t 0 ]; then
            echo "Reading from stdin, press ^D to stop"
        else
            log "Reading from stdin"
        fi

        # preserve trailing newlines
        data=$(cat; echo x)
        data=${data%x}
        filesize=$(($(echo -ne "$data" | wc -c)))
        echo "data: $filesize" 1>&3
        echo -n "$data" 1>&3
    else
        echo "data: 0" 1>&3
    fi

    echo 1>&3
}

function handle_connection {
    local cmd
    local name
    local value
    local token
    local tmp
    local content

    while read -r 0<&3; do
        REPLY="${REPLY#"${REPLY%%[![:space:]]*}"}"
        REPLY="${REPLY%"${REPLY##*[![:space:]]}"}"

        cmd=$REPLY

        token=""
        tmp=""

        while read -r 0<&3; do
            REPLY="${REPLY#"${REPLY%%[![:space:]]*}"}"
            REPLY="${REPLY%"${REPLY##*[![:space:]]}"}"

            if [ "$REPLY" = "" ]; then
                break
            fi

            name="${REPLY%%:*}"
            value="${REPLY##*:}"
            value="${value#"${value%%[![:space:]]*}"}"      # fix textmate syntax highlighting: "

            case $name in
                "token")
                    token=$value
                    ;;
                "data")
                    if [ "$tmp" = "" ]; then
                        tmp="$tmp_dir/rmate.$RANDOM.$$"
                        touch "$tmp"
                    fi

                    dd bs=1 count=$value <&3 >>"$tmp" 2>/dev/null
                    ;;
                *)
                    ;;
            esac
        done

        if [[ "$cmd" = "close" ]]; then
            log "Closing $token"
            if [[ "$token" == "-" ]]; then
                echo -n "$content"
            fi
        elif [[ "$cmd" = "save" ]]; then
            log "Saving $token"
            if [ "$token" != "-" ]; then
                cat "$tmp" > "$token"
            else
                content=$(cat "$tmp")
            fi
            rm "$tmp"
        fi
    done

    log "Done"
}

# connect to textmate and send command
#
exec 3<> "/dev/tcp/$host/$port"

if [ $? -gt 0 ]; then
    echo "Unable to connect to TextMate on $host:$port"
    exit 1
fi

read -r server_info 0<&3

log $server_info

for i in "${!filepaths[@]}"; do
    open_file "$i"
    [[ $? = 0 ]] && new=false && success=true
done

[[ $success = false ]] && exit 1

echo "." 1>&3

if [[ $nowait = true ]]; then
    exec </dev/null >/dev/null 2>/dev/null
    ( (handle_connection &) &)
else
    handle_connection
fi
